package com.mes.common.datashard.intercepet;

import cn.hutool.core.bean.BeanUtil;
import com.alibaba.fastjson2.JSON;
import com.mes.common.core.utils.StringUtils;
import com.mes.common.datashard.strategy.TableShard;
import com.mes.common.datashard.exception.ShardException;
import com.mes.common.datashard.strategy.TableShardStrategy;
import com.mes.common.datashard.util.TableShardSelector;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.MapUtils;
import org.apache.ibatis.binding.MapperMethod;
import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.plugin.Intercepts;
import org.apache.ibatis.plugin.Invocation;
import org.apache.ibatis.plugin.Signature;
import org.apache.ibatis.reflection.DefaultReflectorFactory;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.reflection.ReflectorFactory;
import org.apache.ibatis.reflection.SystemMetaObject;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import java.lang.reflect.Method;
import java.sql.Connection;
import java.util.List;
import java.util.Map;

@Intercepts({
        @Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class})
})
@Component
@Slf4j
public class ShardInterceptor implements Interceptor {

    @Value("${table.shard.enabled:false}")
    private boolean shardSign;//分表标志 可以从配置开启关闭

    @Value("${table.shard.owner}")
    private String owner;//所属库

    @Autowired
    private TableShardSelector tableShardSelector;//分表策略选择器

    private static final ReflectorFactory defaultReflectorFactory = new DefaultReflectorFactory();
    private static final String DELEGATE_MAPPER_STATEMENT = "delegate.mappedStatement";
    private static final String DELEGATE_BOUND_SQL = "delegate.boundSql";
    private static final String DELEGATE_BOUND_SQL_SQL = "delegate.boundSql.sql";

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        //获取StatementHandler
        StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
        //获取mapper元信息
        MetaObject metaObject = MetaObject.forObject(statementHandler,
                SystemMetaObject.DEFAULT_OBJECT_FACTORY,
                SystemMetaObject.DEFAULT_OBJECT_WRAPPER_FACTORY,
                defaultReflectorFactory);
        //获取MappedStatement
        MappedStatement mappedStatement = (MappedStatement) metaObject.getValue(DELEGATE_MAPPER_STATEMENT);
        //获取SQL元数据
        BoundSql boundSql = (BoundSql) metaObject.getValue(DELEGATE_BOUND_SQL);
        //拦截分表逻辑，获取注解
        TableShard annotation = getAnnotation(mappedStatement);
        //当前方法不包含注解，走默认
        if (annotation == null) {
            return invocation.proceed();
        }
        //获取注解参数
        String[] shardStrategys = annotation.tableShardStrategy();
        //获取入参值，根据入参值进行分表
        Object[] columnValues = getColumnValue(annotation.columns(), boundSql);
        //获取替换表名称
        String[] tableNames = annotation.tableName();
        //获取默认表名称
        String[] defaultName = annotation.defaultName();
        //新表名称
        String[] newTableNames = new String[tableNames.length];
        //是否开启分表
        if (shardSign) {
            for (int i = 0; i < columnValues.length; i++) {
                //选择生成表策略
                TableShardStrategy select = tableShardSelector.select(shardStrategys[i]);
                //参数为null时走默认表
                newTableNames[i] = columnValues[i] == null ? defaultName[i] : select.getTableShardName(defaultName[i], columnValues[i]);
            }
        } else {
            //未分表走默认表
            newTableNames = defaultName;
        }
        //获取sql
        String sql = boundSql.getSql();
        if (StringUtils.isEmpty(owner)) {
            log.error("请配置分表库名!!!");
            throw new RuntimeException("请配置分表库名");
        }
        //是否开启建表操作
        boolean tableShardOpen = annotation.tableShardOpen();
        if (tableShardOpen) {  //开启分表 表不存在创建表
            for (int i = 0; i < newTableNames.length; i++) {
                String tab = newTableNames[i];
                if (!tableShardSelector.containsTable(owner, tab)) {
                    //保证获取同一锁对象，在执行并行时单线程创建表(多机可以选择分布式锁)
                    synchronized (tab.intern()) {
                        if (!tableShardSelector.containsTable(owner, tab)) {
                            //创建表
                            tableShardSelector.createTable(defaultName[i], owner, tab);
                        }
                    }
                }
            }
        } else {
            //查询操作不存在则用默认表替换
            for (int i = 0; i < newTableNames.length; i++) {
                String tab = newTableNames[i];
                newTableNames[i] = tableShardSelector.containsTable(owner, tab) ? tab : defaultName[i];
            }
        }
        //替换表名
        for (int i = 0; i < tableNames.length; i++) {
            sql = sql.replace(tableNames[i], newTableNames[i]);
        }
        //拦截并替换sql
        metaObject.setValue(DELEGATE_BOUND_SQL_SQL, sql);
        return invocation.proceed();
    }

    private TableShard getAnnotation(MappedStatement mappedStatement) throws ClassNotFoundException {
        String id = mappedStatement.getId();
        String className = id.substring(0, id.lastIndexOf("."));
        String methodName = id.substring(id.lastIndexOf(".") + 1);
        //获取pageHelper拦截方法
        if (methodName.endsWith("_COUNT")) {
            methodName = methodName.replace("_COUNT", "");
        }
        Class<?> aClass = Class.forName(className);
        Method[] declaredMethods = aClass.getDeclaredMethods();
        for (Method declaredMethod : declaredMethods) {
            if (declaredMethod.getName().equals(methodName)) {
                return declaredMethod.getAnnotation(TableShard.class);
            }
        }
        return null;
    }

    /**
     * 获取sql参数值
     *
     * @param columns  参数名称
     * @param boundSql sql元数据
     * @return 参数列表
     */
    private Object[] getColumnValue(String[] columns, BoundSql boundSql) {
        Object parameterObject = boundSql.getParameterObject();
        if (parameterObject == null) {
            log.info("ShardInterceptor parameterObject is null!!");
            throw new ShardException("分表参数异常！");
        }
        Map map;
        //判断参数类型获取参数值
        if (parameterObject instanceof MapperMethod.ParamMap) {//查询条件是List
            MapperMethod.ParamMap paramMap = (MapperMethod.ParamMap) parameterObject;
            List<Map> list = JSON.parseArray(JSON.toJSONString(paramMap.get("entities")), Map.class);
            map = list.get(0);
        } else if (parameterObject instanceof Map) {//查询条件是Map
            map = (Map) parameterObject;
        } else {//查询条件是其他bean
            map = BeanUtil.beanToMap(parameterObject);
        }
        Object[] columnValue = new Object[columns.length];
        for (int i = 0; i < columns.length; i++) {
            columnValue[i] = MapUtils.getObject(map, columns[i]);
        }
        return columnValue;
    }
}

